Merged [21089]:
[adiumx.git] / Frameworks / Adium Framework / Source / AIEmoticonPack.m
blob6467a1b6b80953dcb2b4be574ec645110407ab49
1 /* 
2  * Adium is the legal property of its developers, whose names are listed in the copyright file included
3  * with this source distribution.
4  * 
5  * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6  * General Public License as published by the Free Software Foundation; either version 2 of the License,
7  * or (at your option) any later version.
8  * 
9  * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10  * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
11  * Public License for more details.
12  * 
13  * You should have received a copy of the GNU General Public License along with this program; if not,
14  * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
15  */
17 #import <Adium/AIEmoticon.h>
18 #import <Adium/AIEmoticonPack.h>
19 #import <Adium/AIEmoticonControllerProtocol.h>
20 #import <AIUtilities/AIFileManagerAdditions.h>
21 #import <AIUtilities/AIImageAdditions.h>
23 #define EMOTICON_PATH_EXTENSION                 @"emoticon"
24 #define EMOTICON_PACK_TEMP_EXTENSION    @"AdiumEmoticonOld"
26 #define EMOTICON_PLIST_FILENAME                 @"Emoticons.plist"
27 #define EMOTICON_PACK_VERSION                   @"AdiumSetVersion"
28 #define EMOTICON_LIST                                   @"Emoticons"
30 #define EMOTICON_EQUIVALENTS                    @"Equivalents"
31 #define EMOTICON_NAME                                   @"Name"
33 #define EMOTICON_SERVICE_CLASS                  @"Service Class"
35 #define EMOTICON_LOCATION                               @"Location"
36 #define EMOTICON_LOCATION_SEPARATOR             @"////"
38 @interface AIEmoticonPack (PRIVATE)
39 - (AIEmoticonPack *)initFromPath:(NSString *)inPath;
40 - (void)setEmoticonArray:(NSArray *)inArray;
41 - (void)loadEmoticons;
42 - (void)loadAdiumEmoticons:(NSDictionary *)emoticons localizedStrings:(NSDictionary *)localizationDict;
43 - (void)loadProteusEmoticons:(NSDictionary *)emoticons;
44 - (void)_upgradeEmoticonPack:(NSString *)packPath;
45 - (NSString *)_imagePathForEmoticonPath:(NSString *)inPath;
46 - (NSArray *)_equivalentsForEmoticonPath:(NSString *)inPath;
47 - (NSString *)_stringWithMacEndlines:(NSString *)inString;
48 @end
51 /*!
52  * @class AIEmoticonPack
53  * @brief Class to encapsulate an emoticon pack, which is a themed collection of emoticons
54  *
55  * An emoticon pack must have a name and a set of one or more emoticons (AIEmoticon objects).
56  * It may also have a serviceClass, which indicates the class of a service upon which its emoticons are preferred.
57  * For example, a set of MSN emoticons would have a service class of @"MSN".
58  */
59 @implementation AIEmoticonPack
61 /*!
62  * @brief Create a new emoticon pack
63  * @param inPath The path to the root of a bundle of emoticons
64  */
65 + (id)emoticonPackFromPath:(NSString *)inPath
67     return [[[self alloc] initFromPath:inPath] autorelease];
70 //Init
71 - (AIEmoticonPack *)initFromPath:(NSString *)inPath
73     if ((self = [super init])) {
74                 path = [inPath retain];
76                 bundle = [[NSBundle bundleWithPath:path] retain];
78                 /*
79                 if (xtraBundle && ([[xtraBundle objectForInfoDictionaryKey:@"XtraBundleVersion"] intValue] == 1)) {
80                         //This checks for a new-style xtra
81                         //New style xtras store the same info, but it's in Contents/Resources/ so that we can have an info.plist file and use NSBundle.
82                         emoticonLocation = [[xtraBundle resourcePath] retain];
83                 } 
84                  */
86                 NSString *localizedName;
87                 name = [[path lastPathComponent] stringByDeletingPathExtension];
88                 if ((localizedName = [[bundle localizedInfoDictionary] objectForKey:name])) {
89                         name = localizedName;
90                 }
91                 [name retain];
93                 emoticonArray = nil;
94                 enabledEmoticonArray = nil;
95                 
96                 enabled = NO;
97         }
98     
99     return self;
102 //Dealloc
103 - (void)dealloc
105     [path release];
106         [bundle release];
107     [name release];
108     [emoticonArray release];
109         [enabledEmoticonArray release];
110         [serviceClass release];
112     [super dealloc];
116  * @brief Name, for display to the user
117  */
118 - (NSString *)name
120     return name;
124  * @brief Path to this emoticon pack
125  */
126 - (NSString *)path
128     return path;
132  * @brief Service class of this emoticon pack
134  * @result A service class, or nil if the emoticon pack is not associated with any service class
135  */
136 - (NSString *)serviceClass
138         return serviceClass;
142  * @brief An array of AIEmoticon objects
143  */
144 - (NSArray *)emoticons
146         if (!emoticonArray) [self loadEmoticons];
147         return emoticonArray;
151  * @brief An array of enabled AIEmoticon objects
152  */
153 - (NSArray *)enabledEmoticons
155         NSEnumerator    *enumerator;
156         AIEmoticon              *emo;
157         
158         if (!enabledEmoticonArray) {
159                 enabledEmoticonArray = [[NSMutableArray alloc] init];
160                 enumerator = [[self emoticons] objectEnumerator];
161                 while ((emo = [enumerator nextObject])) {
162                         if ([emo isEnabled])
163                                 [enabledEmoticonArray addObject:emo];
164                 }
165         }
166         
167         return enabledEmoticonArray;
171  * @brief Return the preview image to use within a menu for this emoticon
173  * It tries to be the emoticon for text equivalent :) or :-). Failing that, any emoticon will do.
174  */
175 - (NSImage *)menuPreviewImage
177         NSArray          *myEmoticons = [self emoticons];
178         NSEnumerator *enumerator;
179         AIEmoticon       *emoticon;
181         enumerator = [myEmoticons objectEnumerator];
182         while ((emoticon = [enumerator nextObject])) {
183                 NSArray *equivalents = [emoticon textEquivalents];
184                 if ([equivalents containsObject:@":)"] || [equivalents containsObject:@":-)"]) {
185                         break;
186                 }
187         }
189         //If we didn't find a happy emoticon, use the first one in the array
190         if (!emoticon && [myEmoticons count]) {
191                 emoticon = [myEmoticons objectAtIndex:0];
192         }
194         return [[emoticon image] imageByScalingForMenuItem];
198  * @brief Set the emoticons that are disabled in this pack
199  * @param inArray An NSArray of AIEmoticon objects to disable
200  */
201 - (void)setDisabledEmoticons:(NSArray *)inArray
203     NSEnumerator    *enumerator;
204     AIEmoticon      *emoticon;
205     
206     //Flag our emoticons as enabled/disabled
207     enumerator = [[self emoticons] objectEnumerator];
208     while ((emoticon = [enumerator nextObject])) {
209         [emoticon setEnabled:(![inArray containsObject:[emoticon name]])];
210     }
211         
212         //reset the emabled emoticon list
213         if (enabledEmoticonArray) {
214                 [enabledEmoticonArray release];
215                 enabledEmoticonArray = nil;
216         }
220  * @brief Enable/Disable this pack
221  * @param inEnabled Should this pack be enabled?
222  */
223 - (void)setIsEnabled:(BOOL)inEnabled
225         enabled = inEnabled;
229  * @brief Is this pack enabled?
230  */
231 - (BOOL)isEnabled{
232         return enabled;
235 //Copying --------------------------------------------------------------------------------------------------------------
236 #pragma mark Copying
237 //Copy
238 - (id)copyWithZone:(NSZone *)zone
240     AIEmoticonPack      *newPack = [[AIEmoticonPack alloc] initFromPath:path];   
242         newPack->emoticonArray = [emoticonArray mutableCopy];
243         newPack->serviceClass = [serviceClass retain];
244         newPack->path = [path retain];
245         newPack->bundle = [bundle retain];
246         newPack->name = [name retain];
248     return newPack;
251 //Loading Emoticons ----------------------------------------------------------------------------------------------------
252 #pragma mark Loading Emoticons
254  * @brief Load the emoticons in this pack.
256  * Called by [self emoticons] as needed
257  */
258 - (void)loadEmoticons
260         [emoticonArray release]; emoticonArray = [[NSMutableArray alloc] init];
261         [serviceClass release]; serviceClass = nil;
263         //
264         NSString                *infoDictPath = [[bundle resourcePath] stringByAppendingPathComponent:EMOTICON_PLIST_FILENAME];
265         NSDictionary    *infoDict = [NSDictionary dictionaryWithContentsOfFile:infoDictPath];
266         NSDictionary    *localizedInfoDict = [[NSBundle bundleWithPath:path] localizedInfoDictionary];
268         //If no info dict was found, assume that this is an old emoticon pack and try to upgrade it
269         if (!infoDict) {
270                 [self _upgradeEmoticonPack:path];
271                 infoDict = [NSDictionary dictionaryWithContentsOfFile:infoDictPath];
272                 [bundle release]; bundle = [[NSBundle bundleWithPath:path] retain];
273         }
275         //Load the emoticons
276         if (infoDict) {
277                 /* Handle optional location key, which allows emoticons to be loaded
278                  * from arbitrary directories. This is only used by the iChat emoticon
279                  * pack.
280                  */
281                 id possiblePaths = [infoDict objectForKey:EMOTICON_LOCATION];
282                 if (possiblePaths) {
283                         if ([possiblePaths isKindOfClass:[NSString class]]) {
284                                 possiblePaths = [NSArray arrayWithObjects:possiblePaths, nil];
285                         }
287                         NSEnumerator *pathEnumerator = [possiblePaths objectEnumerator];
288                         NSString *aPath;
290                         while ((aPath = [pathEnumerator nextObject])) {
291                                 NSString *possiblePath;
292                                 NSArray *splitPath = [aPath componentsSeparatedByString:EMOTICON_LOCATION_SEPARATOR];
294                                 /* Two possible formats:
295                                  *
296                                  * <string>/absolute/path/to/directory</string>
297                                  * <string>CFBundleIdentifier////relative/path/from/bundle/to/directory</string>
298                                  *
299                                  * The separator in the latter is ////, defined as EMOTICON_LOCATION_SEPARATOR.
300                                  */
301                                 if ([splitPath count] == 1) {
302                                         possiblePath = [splitPath objectAtIndex:0];
303                                 } else {
304                                         NSArray *components = [NSArray arrayWithObjects:
305                                                 [[NSWorkspace sharedWorkspace] absolutePathForAppBundleWithIdentifier:[splitPath objectAtIndex:0]],
306                                                 [splitPath objectAtIndex:1],
307                                                 nil];
308                                         possiblePath = [NSString pathWithComponents:components];
309                                 }
311                                 /* If the directory exists, then we've found the location. If we
312                                  * make it all the way through the list without finding a valid
313                                  * directory, then the standard location will be used.
314                                  */
315                                 BOOL isDir;
316                                 if ([[NSFileManager defaultManager] fileExistsAtPath:possiblePath isDirectory:&isDir] && isDir) {
317                                         [bundle release];
318                                         bundle = [[NSBundle bundleWithPath:possiblePath] retain];
319                                         break;
320                                 }
321                         }
322                 }
324                 int version = [[infoDict objectForKey:EMOTICON_PACK_VERSION] intValue];
325                 
326                 switch (version) {
327                         case 0: [self loadProteusEmoticons:infoDict]; break;
328                         case 1: [self loadAdiumEmoticons:[infoDict objectForKey:EMOTICON_LIST] localizedStrings:localizedInfoDict]; break;
329                         default: break;
330                 }
331                 
332                 serviceClass = [[infoDict objectForKey:EMOTICON_SERVICE_CLASS] retain];
333                 if (!serviceClass) {
334                         if ([name rangeOfString:@"AIM"].location != NSNotFound) {
335                                 serviceClass = [@"AIM-compatible" retain];
336                         } else if ([name rangeOfString:@"MSN"].location != NSNotFound) {
337                                 serviceClass = [@"MSN" retain];
338                         } else if ([name rangeOfString:@"Yahoo"].location != NSNotFound) {
339                                 serviceClass = [@"Yahoo!" retain];
340                         }
341                 }
342         }
343         
344         //Sort the emoticons in this pack using the AIEmoticon compare: selector
345         [emoticonArray sortUsingSelector:@selector(compare:)];
349  * @brief Load an Adium version 1 emoticon pack
351  * @param emoticons A dictionary whose keys are file names and objects are themselves dictionaries with equivalent and name information.
352  */
353 - (void)loadAdiumEmoticons:(NSDictionary *)emoticons localizedStrings:(NSDictionary *)localizationDict
355         NSEnumerator    *enumerator = [emoticons keyEnumerator];
356         NSString                *fileName;
357         NSBundle                *myBundle = (!localizationDict ? [NSBundle bundleForClass:[self class]] : nil);
359         while ((fileName = [enumerator nextObject])) {
360                 id      dict = [emoticons objectForKey:fileName];
362                 if ([dict isKindOfClass:[NSDictionary class]]) {
363                         NSString *emoticonName = [(NSDictionary *)dict objectForKey:EMOTICON_NAME];
364                         NSString *localizedEmoticonName = nil;
366                         if (emoticonName) {
367                                 if (localizationDict) {
368                                         //If the bundle provides localizations, use them
369                                         localizedEmoticonName = [localizationDict objectForKey:emoticonName];
370                                 } 
371                                 
372                                 if (!localizedEmoticonName) {
373                                         //Otherwise, look at our list of default translations (generated at the bottom of this file)
374                                         localizedEmoticonName = [myBundle localizedStringForKey:emoticonName
375                                                                                                                                           value:emoticonName
376                                                                                                                                           table:@"EmoticonNames"];
377                                 }
378                                 
379                                 if (localizedEmoticonName)
380                                         emoticonName = localizedEmoticonName;
381                         }
383                         [emoticonArray addObject:[AIEmoticon emoticonWithIconPath:[bundle pathForImageResource:fileName]
384                                                                                                                   equivalents:[(NSDictionary *)dict objectForKey:EMOTICON_EQUIVALENTS]
385                                                                                                                                  name:emoticonName
386                                                                                                                                  pack:self]];
387                 }
388         }
392  * @brief Load a Proteus emoticon pack
393  */
394 - (void)loadProteusEmoticons:(NSDictionary *)emoticons
396         NSEnumerator    *enumerator = [emoticons keyEnumerator];
397         NSString                *fileName;
398         
399         while ((fileName = [enumerator nextObject])) {
400                 NSDictionary    *dict = [emoticons objectForKey:fileName];
401                 
402                 [emoticonArray addObject:[AIEmoticon emoticonWithIconPath:[bundle pathForImageResource:fileName]
403                                                                                                           equivalents:[dict objectForKey:@"String Representations"]
404                                                                                                                          name:[dict objectForKey:@"Meaning"]
405                                                                                                                          pack:self]];
406         }
410  * @brief Flush any cached emoticon images (and image attachment strings)
411  */
412 - (void)flushEmoticonImageCache
414     NSEnumerator    *enumerator;
415     AIEmoticon      *emoticon;
416     
417     //Flag our emoticons as enabled/disabled
418     enumerator = [[self emoticons] objectEnumerator];
419     while ((emoticon = [enumerator nextObject])) {
420         [emoticon flushEmoticonImageCache];
421     }
425 //Upgrading ------------------------------------------------------------------------------------------------------------
426 //Methods for opening and converting old format Adium emoticon packs
427 #pragma mark Upgrading
429  * @brief Upgrade an emoticon pack from the old format (where every emoticon is a separate file) to the new format
430  */
431 - (void)_upgradeEmoticonPack:(NSString *)packPath
433         NSString                                *packName, *workingDirectory, *tempPackName, *tempPackPath, *fileName;
434         NSDirectoryEnumerator   *enumerator;
435         NSFileManager           *mgr = [NSFileManager defaultManager];
436         NSMutableDictionary             *infoDict = [NSMutableDictionary dictionary];
437         NSMutableDictionary             *emoticonDict = [NSMutableDictionary dictionary];
438         
439         //
440         packName = [[packPath lastPathComponent] stringByDeletingPathExtension];
441         workingDirectory = [packPath stringByDeletingLastPathComponent];
442         
443         //Rename the existing pack to .AdiumEmoticonOld
444         tempPackName = [packName stringByAppendingPathExtension:EMOTICON_PACK_TEMP_EXTENSION];
445         tempPackPath = [workingDirectory stringByAppendingPathComponent:tempPackName];
446         [mgr movePath:packPath toPath:tempPackPath handler:nil];
447         
448         //Create ourself a new pack
449         [mgr createDirectoryAtPath:packPath attributes:nil];
450         
451         //Version this pack as 1
452         [infoDict setObject:[NSNumber numberWithInt:1] forKey:EMOTICON_PACK_VERSION];
453         
454         //Process all .emoticons in the old pack
455         enumerator = [[NSFileManager defaultManager] enumeratorAtPath:tempPackPath];
456         while ((fileName = [enumerator nextObject])) {        
457                 if ([[fileName lastPathComponent] characterAtIndex:0] != '.' &&
458                    [[fileName pathExtension] caseInsensitiveCompare:EMOTICON_PATH_EXTENSION] == 0) {
459                         NSString        *emoticonPath = [tempPackPath stringByAppendingPathComponent:fileName];
460                         BOOL            isDirectory;
461                         
462                         //Ensure that this is a folder and that it is non-empty
463                         [mgr fileExistsAtPath:emoticonPath isDirectory:&isDirectory];
464                         if (isDirectory) {
465                                 NSString        *emoticonName = [fileName stringByDeletingPathExtension];
466                                 
467                                 //Get the text equivalents out of this .emoticon
468                                 NSArray         *emoticonStrings = [self _equivalentsForEmoticonPath:emoticonPath];
469                                 
470                                 //Get the image out of this .emoticon
471                                 NSString        *imagePath = [self _imagePathForEmoticonPath:emoticonPath];
472                                 NSString        *imageExtension = [imagePath pathExtension];
473                                 
474                                 if (emoticonStrings && imagePath) {
475                                         NSString        *newImageName = [emoticonName stringByAppendingPathExtension:imageExtension];
476                                         
477                                         //Move the image into our new pack (with a unique name)
478                                         NSString        *newImagePath = [packPath stringByAppendingPathComponent:newImageName];
479                                         [mgr copyPath:imagePath toPath:newImagePath handler:nil];
480                                         
481                                         //Add to our emoticon plist
482                                         [emoticonDict setObject:[NSDictionary dictionaryWithObjectsAndKeys:
483                                                 emoticonStrings, EMOTICON_EQUIVALENTS,
484                                                 emoticonName, EMOTICON_NAME, nil] 
485                                                                          forKey:newImageName];
486                                 }
487                         }
488                 }
489         }
490         
491         //Write our plist to the new pack
492         [infoDict setObject:emoticonDict forKey:EMOTICON_LIST];
493         [infoDict writeToFile:[packPath stringByAppendingPathComponent:EMOTICON_PLIST_FILENAME] atomically:NO];
494         
495         //Move the old/temp pack to the trash
496         [mgr trashFileAtPath:tempPackPath];
500  * @brief Path to an emoticon image
502  * @param Path within which to search for a file whose name starts with "Emoticon"
503  */
504 - (NSString *)_imagePathForEmoticonPath:(NSString *)inPath
506     NSDirectoryEnumerator   *enumerator;
507     NSString                    *fileName;
508     
509     //Search for the file named Emoticon in our bundle (It can be in any image format)
510     enumerator = [[NSFileManager defaultManager] enumeratorAtPath:inPath];
511     while ((fileName = [enumerator nextObject])) {
512                 if ([fileName hasPrefix:@"Emoticon"]) return [inPath stringByAppendingPathComponent:fileName];
513     }
514     
515     return nil;
519  * @brief Retrieve the text equivalents from a pack
520  */
521 - (NSArray *)_equivalentsForEmoticonPath:(NSString *)inPath
523         NSString    *equivFilePath = [inPath stringByAppendingPathComponent:@"TextEquivalents.txt"];
524         NSArray         *textEquivalents = nil;
525         
526         //Fetch the text equivalents
527         if ([[NSFileManager defaultManager] fileExistsAtPath:equivFilePath]) {
528                 NSString        *equivString;
529                 
530                 //Convert the text file into an array of strings
531                 equivString = [NSMutableString stringWithContentsOfFile:equivFilePath];
532                 equivString = [self _stringWithMacEndlines:equivString];
533                 textEquivalents = [[equivString componentsSeparatedByString:@"\r"] retain];
534         }
535         
536         return textEquivalents;
540  * @brief Convert any unix/windows line endings to mac line endings
541  * @result The converted string
542  */
543 - (NSString *)_stringWithMacEndlines:(NSString *)inString
545     NSCharacterSet      *newlineSet = [NSCharacterSet characterSetWithCharactersInString:@"\n"];
546     NSMutableString     *newString = nil; //We avoid creating a new string if not necessary
547     NSRange             charRange;
548     
549     //Step through all the invalid endlines
550     charRange = [inString rangeOfCharacterFromSet:newlineSet];
551     while (charRange.length != 0) {
552         if (!newString) newString = [[inString mutableCopy] autorelease];
553                 
554         //Replace endline and continue
555         [newString replaceCharactersInRange:charRange withString:@"\r"];
556         charRange = [newString rangeOfCharacterFromSet:newlineSet];
557     }
558     
559     return newString ? newString : inString;
562 - (NSString *)description
564         return ([NSString stringWithFormat:@"[%@: %@, ServiceClass %@]",[super description], [self name], [self serviceClass]]);
567 /* Localized emoticon names, listed here for genstrings:
569 AILocalizedStringFromTable(@"Angry", "EmoticonNames", "Emoticon name")
570 AILocalizedStringFromTable(@"Blush", "EmoticonNames", "Emoticon name")
571 AILocalizedStringFromTable(@"Cry", "EmoticonNames", "Emoticon name")
572 AILocalizedStringFromTable(@"Scared", "EmoticonNames", "Emoticon name")
573 AILocalizedStringFromTable(@"Sad", "EmoticonNames", "Emoticon name")
574 AILocalizedStringFromTable(@"Gasp", "EmoticonNames", "Emoticon name")
575 AILocalizedStringFromTable(@"Grin", "EmoticonNames", "Emoticon name")
576 AILocalizedStringFromTable(@"Angel", "EmoticonNames", "Emoticon name")
577 AILocalizedStringFromTable(@"Kiss", "EmoticonNames", "Emoticon name")
578 AILocalizedStringFromTable(@"Lips Are Sealed", "EmoticonNames", "Emoticon name")
579 AILocalizedStringFromTable(@"Money-mouth", "EmoticonNames", "Emoticon name")
580 AILocalizedStringFromTable(@"Smile", "EmoticonNames", "Emoticon name")
581 AILocalizedStringFromTable(@"Sticking Out Tongue", "EmoticonNames", "Emoticon name")
582 AILocalizedStringFromTable(@"Erm", "EmoticonNames", "Emoticon name")
583 AILocalizedStringFromTable(@"Cool", "EmoticonNames", "Emoticon name")
584 AILocalizedStringFromTable(@"Wink", "EmoticonNames", "Emoticon name")
585 AILocalizedStringFromTable(@"Foot In Mouth", "EmoticonNames", "Emoticon name")
586 AILocalizedStringFromTable(@"Frown", "EmoticonNames", "Emoticon name")
587 AILocalizedStringFromTable(@"Confused", "EmoticonNames", "Emoticon name")
588 AILocalizedStringFromTable(@"Halo", "EmoticonNames", "Emoticon name")
589 AILocalizedStringFromTable(@"Undecided", "EmoticonNames", "Emoticon name")
590 AILocalizedStringFromTable(@"Embarrassed", "EmoticonNames", "Emoticon name")
594 @end